import csv
import multiprocessing
import os
import random
import sqlite3 as lite
from dataclasses import dataclass, field

import datetime
from enum import Enum
from itertools import product
from typing import Callable
import copy
import time

import pandas as pd

from configpath import sqlite_stra_rslt, sqlite_stra_conf, csv_file_path
from constant import Direction, Offset, StopOrderStatus, IntervalType, DirectType, OffsetType

# from BQ.entity.stra_entity import Bar

long_cond_5pv = [[14, 1], [17, 1], [20, 1], [14, 2], [17, 2], [20, 2], [13, 5], [19, 6],
                 [11, 8], [12, 8], [15, 8], [18, 8], [11, 9], [12, 9], [18, 9],
                 [4, 13], [10, 13], [4, 14], [7, 14], [10, 14], [3, 17], [6, 17],
                 [9, 17], [1, 20], [2, 20], [5, 20], [8, 20]]
short_cond_5pv = [[20, 3], [14, 4], [17, 4], [20, 4], [13, 7], [16, 7], [19, 7],
                  [11, 10], [12, 10], [15, 10], [18, 10], [4, 11], [7, 11], [10, 11],
                  [4, 12], [7, 12], [10, 12], [3, 15], [9, 15], [9, 16], [1, 18],
                  [2, 18], [5, 18], [8, 18], [1, 19], [8, 19]]

long_cond_4pv = [1, 2, 3, 5, 6, 8, 9, 14, 17, 20]
short_cond_4pv = [11, 12, 13, 15, 16, 18, 19, 4, 7, 10]
long_stop_cond_zc = [11, 12, 13, 14, 15, 16, 17, 18, 19, 20]  # 当开多时的 zig_cycle 在此列表中时要设置止损
short_stop_cond_zc = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]  # 当开空时的 zig_cycle 在此列表中时要设置止损
long_stop_cond_pv = [1]  # 当峰被发现时开多要止损
short_stop_cond_pv = [-1]  # 当谷被发现时开空要止损


def save_signals(table_name, data: pd.DataFrame = None):
    """"保存信号到sqlite db 中"""
    with lite.connect(sqlite_stra_rslt) as cnn:
        data.to_sql(table_name, cnn, if_exists='replace')


def save_stra_conf(stra, conf=None):
    """保存策略的配置、优化过的参数、多空开平仓条件等，由具体策略在参数优化后调用"""
    create_sql = """
                create table if not exists [stra_conf](
                [id] varchar(100) not null  primary key,
                [class_name] text,
                [symbol] varchar(100),
                [period] int,
                [params] text,
                [conf] text,
                [data_datetime] text,
                update_datetime TIMESTAMP DEFAULT (datetime('now', 'localtime')),
                [vars] text null                
                );"""
    if not conf:
        conf = stra.conf
    params = stra.get_parameters()
    variables = stra.get_vars()  # 若没有变量，vars={}

    data_datetime = {}
    data_len = getattr(stra.stra_engine, 'init_data_len', 0)
    if data_len == 0:
        data_datetime['start_dt'] = getattr(stra.stra_engine, 'start_dt', '')
        data_datetime['end_dt'] = getattr(stra.stra_engine, 'end_dt', '')
    else:
        data_datetime['data_len'] = data_len

    with lite.connect(sqlite_stra_conf) as cnn:
        stra_id = '_'.join([stra.__class__.__name__, stra.symbol, str(stra.period)])
        sql = f"""
                insert or replace into [stra_conf]([id],[symbol],[period],[params],[conf],[data_datetime])
                values("{stra_id}","{stra.symbol}",{stra.period},"{str(params)}","{str(conf)}",
                "{str(data_datetime)}")"""
        if variables:
            sql = f"""
                    insert or replace into [stra_conf]([id],[symbol],[period],[params],[conf],[data_datetime],[vars])
                    values("{stra_id}","{stra.symbol}",{stra.period},"{str(params)}","{str(conf)}",
                    "{str(data_datetime)}","{str(variables)}")"""
        cnn.executescript(create_sql)
        cnn.commit()
        cnn.execute(sql)
        stra.conf_saved = True


def load_conf(stra):
    """读取保存的配置及参数，由具体策略调用"""
    with lite.connect(sqlite_stra_conf) as cnn:
        sql = f"""select [id],[symbol],[period],[params],[conf] 
            from stra_conf 
            where [symbol]='{stra.symbol}' and [period]={stra.period} 
            and class_name ='{stra.__class__.__name__}')"""
        data = pd.read_sql(sql, cnn)
        if data is not None and not data.empty:
            return data

        return None


def save_stra_signal_csv(stra_name, params, dt, symbol, offset, direction, price, vol, bssc=0, other=''):
    stra_name = os.path.join(csv_file_path, stra_name + '.csv')
    neen_write_header = not os.path.exists(stra_name)
    with open(stra_name, 'a') as csv_file:
        header = ['params', 'datetime', 'symbol', 'direction',
                  'offset', 'price', 'volume', 'bssc', 'other']
        csv_writer = csv.DictWriter(csv_file, header)
        if neen_write_header:
            csv_writer.writeheader()
        data = {'params': params, 'datetime': dt, 'symbol': symbol,
                'direction': direction, 'offset': offset, 'price': price,
                'volume': vol, 'bssc': bssc, 'other': other}
        csv_writer.writerow(data)


class OptimizationSetting:
    """
    Setting for runnning optimization.
    """

    def __init__(self):
        """"""
        self.params = {}
        self.target_name = ""

    def add_parameter(
            self, name: str, start: float, end: float = None, step: float = None
    ):
        """"""
        if not end and not step:
            self.params[name] = [start]
            return

        if start >= end:
            print("参数优化起始点必须小于终止点")
            return

        if step <= 0:
            print("参数优化步进必须大于0")
            return

        value = start
        value_list = []

        while value <= end:
            value_list.append(value)
            value += step

        self.params[name] = value_list

    def set_target(self, target_name: str):
        """"""
        self.target_name = target_name

    def generate_setting(self):
        """"""
        keys = self.params.keys()
        values = self.params.values()
        products = list(product(*values))

        settings = []
        for p in products:
            setting = dict(zip(keys, p))
            settings.append(setting)

        return settings

    def generate_setting_ga(self):
        """为遗传算法准备参数设置"""
        settings_ga = []
        settings = self.generate_setting()
        for d in settings:
            param = [tuple(i) for i in d.items()]
            settings_ga.append(param)
        return settings_ga


def run_optimization(self, optimization_setting: OptimizationSetting, output=True, long_short='b'):
    """"""
    # Get optimization setting and target
    settings = optimization_setting.generate_setting()
    target_name = optimization_setting.target_name

    if not settings:
        self.output("优化参数组合为空，请检查")
        return

    if not target_name:
        self.output("优化目标未设置，请检查")
        return

    # Use multiprocessing pool for running backtesting with different setting
    # Force to use spawn method to create new process (instead of fork on Linux)
    ctx = multiprocessing.get_context("spawn")
    pool = ctx.Pool(multiprocessing.cpu_count())

    results = []
    for setting in settings:
        result = (pool.apply_async(optimize, (
            target_name,
            self.strategy_class,
            setting,
            self.vt_symbol,
            self.interval,
            self.start,
            self.rate,
            self.slippage,
            self.size,
            self.pricetick,
            self.capital,
            self.end,
            self.mode,
            self.inverse
        )))
        results.append(result)

    pool.close_price()
    pool.join()

    # Sort results and output
    result_values = [result.get() for result in results]
    result_values.sort(reverse=True, key=lambda result: result[1])

    if output:
        for value in result_values:
            msg = f"参数：{value[0]}, 目标：{value[1]}"
            self.output(msg)

    return result_values


def run_ga_optimization(self, optimization_setting: OptimizationSetting,
                        population_size=100, ngen_size=30, output=True, long_short='b'):
    """"""
    # Get optimization setting and target
    settings = optimization_setting.generate_setting_ga()
    target_name = optimization_setting.target_name

    if not settings:
        self.output("优化参数组合为空，请检查")
        return

    if not target_name:
        self.output("优化目标未设置，请检查")
        return

    # Define parameter generation function
    def generate_parameter():
        """"""
        return random.choice(settings)

    def mutate_individual(individual, indpb):
        """"""
        size = len(individual)
        paramlist = generate_parameter()
        for i in range(size):
            if random.random() < indpb:
                individual[i] = paramlist[i]
        return individual,

    # Create ga object function
    global ga_target_name
    global ga_strategy_class
    global ga_setting
    global ga_vt_symbol
    global ga_interval
    global ga_start
    global ga_rate
    global ga_slippage
    global ga_size
    global ga_pricetick
    global ga_capital
    global ga_end
    global ga_mode
    global ga_inverse


def optimize(func: Callable, target_setting, **params):
    """"""

